﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Printing;
using System.Linq;
using System.Text;

namespace Dycox.Reporting.Printing
{
    public class ReportPrintDocument : PrintDocument
    {
        public ReportPrintDocument()
        {
        }

        public ReportPrintDocument(Report report)
        {
            Report = report;
        }

        private Report _report;
        private bool _printing;
        private object[] _dataObjects;
        private int _dataObjectIndex;

        [Browsable(false), DefaultValue(null)]
        public Report Report
        {
            get
            {
                return _report;
            }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                if (_report != value)
                {
                    ThrowIfPrinting();
                    _report = value;
                    base.DocumentName = _report.Name;
                    OnReportChanged(EventArgs.Empty);
                }
            }
        }

        public event EventHandler ReportChanged;

        protected virtual void OnReportChanged(EventArgs e)
        {
            if (ReportChanged != null)
                ReportChanged(this, e);
        }

        private void ThrowIfPrinting()
        {
            if (_printing)
                throw new InvalidOperationException("Cannot perform this operation while printing is in procedure.");
        }

        [Browsable(false), DefaultValue("")]
        public new string DocumentName
        {
            get
            {
                if (_report == null)
                    return string.Empty;
                else
                    return _report.Name;
            }
            set
            {
            }
        }

        private IPaperNotFoundHandler _paperNotFoundHandler;

        [Browsable(false), DefaultValue(null)]
        public IPaperNotFoundHandler PaperNotFoundHandler
        {
            get { return _paperNotFoundHandler; }
            set { _paperNotFoundHandler = value; }
        }


        protected override void OnBeginPrint(PrintEventArgs e)
        {
            if (_report == null && !DesignMode)
                throw new InvalidOperationException("Cannot start printing becuase Report property has not been initialized.");

            _printing = true;

            if (_report != null)
            {
                if (_dataSource != null)
                {
                    ICollection coll = _dataSource as ICollection;
                    if (coll != null)
                    {
                        _dataObjects = new object[coll.Count];
                        coll.CopyTo(_dataObjects, 0);
                    }
                    else
                    {
                        _dataObjects = null;
                    }
                }
                else
                    _dataObjects = null;

                _dataObjectIndex = 0;
            }

            base.OnBeginPrint(e);
        }

        protected override void OnEndPrint(PrintEventArgs e)
        {
            base.OnEndPrint(e);
            _printing = false;
        }

        private static RectangleF ToMillieMeter(Rectangle rect)
        {
            RectangleF bounds = new RectangleF(
                PrinterUnitConvert.Convert(rect.Left, PrinterUnit.ThousandthsOfAnInch, PrinterUnit.TenthsOfAMillimeter) / 1,
                PrinterUnitConvert.Convert(rect.Top, PrinterUnit.ThousandthsOfAnInch, PrinterUnit.TenthsOfAMillimeter) / 1,
                PrinterUnitConvert.Convert(rect.Width, PrinterUnit.ThousandthsOfAnInch, PrinterUnit.TenthsOfAMillimeter) / 1,
                PrinterUnitConvert.Convert(rect.Height, PrinterUnit.ThousandthsOfAnInch, PrinterUnit.TenthsOfAMillimeter) / 1
                );

            return bounds;
        }

        protected override void OnPrintPage(PrintPageEventArgs e)
        {
            base.OnPrintPage(e);

            if (_report != null)
            {
                Graphics g = e.Graphics;
                g.PageUnit = GraphicsUnit.Millimeter;
                RectangleF pageBounds = ToMillieMeter(e.PageBounds);
                g.SetClip(pageBounds);
                
                //g.TranslateTransform(pageBounds.Left, pageBounds.Top);//pageBounds.Left, pageBounds.Top);

                RenderingContext context = new RenderingContext(_report, g);
                ReportPageSettings rps = _report.PageSettings;
                bool hasMorePages = false;
                int columns = rps.Columns, c = 0, r = 0, rows = rps.Rows;
                float x = 0, y = 0, h = 0, vs = rps.VerticalSpace, hs = rps.HorizontalSpace;


                while (PrintReport(context))
                {
                    if (rps.PageBreak == PageBreakMode.Report)
                    {
                        hasMorePages = true;
                        break;
                    }

                    SizeF bs = context.BodySize;

                    h = Math.Max(bs.Height, h);

                    if (++c < columns)
                    {
                        x += bs.Width + hs;
                        g.TranslateTransform(bs.Width + hs, 0);
                    }
                    else
                    {
                        y += h + vs;
                        r++;

                        if (rows == 0 && y + bs.Height >= pageBounds.Bottom || rows > 0 && r >= rows)
                        {
                            hasMorePages = true;
                            break;
                        }

                        g.TranslateTransform(-x, h + vs);
                        x = 0;
                        h = c = 0;
                    }

                }
                
                e.HasMorePages = hasMorePages;
            }
        }

        private bool PrintReport(RenderingContext context)
        {
            int count;
            object dataObj;

            if (_dataObjects != null)
            {
                count = _dataObjects.Length;
                dataObj = _dataObjects[_dataObjectIndex++];
            }
            else
            {
                count = -1;
                dataObj = _dataSource;
            }

            context.Render(dataObj, null);

            return _dataObjectIndex < count;
        }


        private bool _throwPaperNotFound;
        [DefaultValue(false), Category("Behavior"), Description("Indicates whether to throw an exception when paper size not defined or incorrect.")]
        public bool ThrowPaperNotFound
        {
            get { return _throwPaperNotFound; }
            set { _throwPaperNotFound = value; }
        }
        
        protected override void OnQueryPageSettings(QueryPageSettingsEventArgs e)
        {
            base.OnQueryPageSettings(e);

            if (_report != null)
            {
                var rps = _report.PageSettings;
                var ps = e.PageSettings;

                var paperSize = GetPaperSize(ps.PrinterSettings);

                if (paperSize != null)
                {
                    ps.PaperSize = paperSize;

                    //if (!rps.Margins.IsEmpty)
                        ps.Margins = rps.CreatePrintingMargins();
                }
                else if (_throwPaperNotFound)
                    throw new InvalidPaperNameException(rps.PaperName);

                ps.Landscape = rps.Landscape;
            }
        }

        private object _dataSource;

        [AttributeProvider(typeof(IListSource)), DefaultValue(null), Category("Data"), Description("Specifies the data source that binding to report when printing.")]
        public object DataSource
        {
            get { return _dataSource; }
            set
            {
                ThrowIfPrinting();
                _dataSource = value;
            }
        }

        private static PaperSize GetPaperSize(ReportPageSettings rps, PrinterSettings printer)
        {
            var ps0 = rps.PaperSizeMM;

            PaperSize size = new PaperSize("A4",
                (int)PrinterUnitConvert.Convert(ps0.Width, PrinterUnit.TenthsOfAMillimeter, PrinterUnit.ThousandthsOfAnInch),
                (int)PrinterUnitConvert.Convert(ps0.Height, PrinterUnit.TenthsOfAMillimeter, PrinterUnit.ThousandthsOfAnInch));

            return size;
        }

        private PaperSize GetPaperSize(PrinterSettings printer)
        {
            ReportPageSettings rps = _report.PageSettings;
            string paperName = rps.PaperName;
            bool retry = true;
            PaperSize sysSize = null;
            
            while (retry)
            {
                sysSize = null;

                foreach (PaperSize size in printer.PaperSizes)
                    if (size.PaperName == paperName)
                    {
                        if (IsPaperSizeMatch(rps, size))
                            return size;

                        sysSize = size;
                        break;
                    }

                PaperNotFoundEventArgs e =
                    new PaperNotFoundEventArgs(_report, printer.PrinterName, sysSize);

                if (_paperNotFoundHandler != null)
                    _paperNotFoundHandler.OnPaperNotFound(this, e);
                else
                    OnPaperNotFound(e);

                retry = e.Retry;
            }

            return sysSize;
        }

        private static bool IsPaperSizeMatch(ReportPageSettings rps, PaperSize paperSize)
        {
            SizeF size1 = ConvertSize(paperSize, ReportPageUnit.Millimeter);
            SizeF size0 = rps.PaperSize;

            if (rps.Unit == ReportPageUnit.Centimeter)
            {
                size0.Width *= 10f;
                size0.Height *= 10f;
            }
            
            return Size.Round(size0) == Size.Round(size1);
        }

        public static SizeF ConvertSize(PaperSize paperSize, ReportPageUnit unit)
        {
            float width = (float)PrinterUnitConvert.Convert(paperSize.Width, PrinterUnit.ThousandthsOfAnInch, PrinterUnit.HundredthsOfAMillimeter) / 10f;
            float height = (float)PrinterUnitConvert.Convert(paperSize.Height, PrinterUnit.ThousandthsOfAnInch, PrinterUnit.HundredthsOfAMillimeter) / 10f;
            if (unit == ReportPageUnit.Centimeter)
            {
                width /= 10f;
                height /= 10f;
            }

            return new SizeF(width, height);
        }

        public static string GetUnitText(ReportPageUnit unit)
        {
            switch (unit)
            {
                case ReportPageUnit.Millimeter:
                    return "mm";
                case ReportPageUnit.Centimeter:
                    return "cm";
                default:
                    throw new ArgumentException();
            }
        }

        public event PaperNotFoundEventHandler PaperNotFound;

        protected virtual void OnPaperNotFound(PaperNotFoundEventArgs e)
        {
            if (PaperNotFound != null)
                PaperNotFound(this, e);
        }

    }
}
